跳到主要内容

Java IO学习-NIO 粘包与半包

调用 ByteBuffer 的方法

ByteBuffer 调试工具类

这里使用黑马教程的写的工具类 需要先导入 netty 依赖

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.66.Final</version>
</dependency>

代码太长了,就放在 Gist 里面了

使用示例:

import static com.alsritter.util.ByteBufferUtil.debugAll;

public class Test {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte) 0x61); // 'a'
debugAll(buffer);
}
}

打印结果如下:

+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 |a......... |
+--------+-------------------------------------------------+----------------+

ByteBuffer 执行方法测试

public class Test {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
String line = "\n===========================================================================================";
System.out.println("向 buffer 中写入1个字节的数据");
buffer.put((byte)97);
// 使用工具类,查看buffer状态
debugAll(buffer);

System.out.println(line);
System.out.println("再向 buffer 中写入4个字节的数据");
buffer.put(new byte[]{98, 99, 100, 101});
debugAll(buffer);

System.out.println(line);
buffer.flip();
System.out.println("取数据前的数据");
debugAll(buffer);
System.out.println(buffer.get());
System.out.println(buffer.get());
System.out.println("查看取得两次数据后 position 的位置");
debugAll(buffer);

System.out.println(line);
System.out.println("使用 compact 切换模式");
buffer.compact();
debugAll(buffer);

System.out.println(line);
System.out.println("再次写入");
buffer.put((byte)102);
buffer.put((byte)103);
debugAll(buffer);
}
}

打印的结果:

向 buffer 中写入1个字节的数据
+--------+-------------------- all ------------------------+----------------+
position: [1], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 |a......... |
+--------+-------------------------------------------------+----------------+

===========================================================================================
再向 buffer 中写入4个字节的数据
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
+--------+-------------------------------------------------+----------------+

===========================================================================================
取数据前的数据
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
+--------+-------------------------------------------------+----------------+
97
98
查看取得两次数据后 position 的位置
+--------+-------------------- all ------------------------+----------------+
position: [2], limit: [5]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 00 00 00 00 00 |abcde..... |
+--------+-------------------------------------------------+----------------+

===========================================================================================
使用 compact 切换模式
+--------+-------------------- all ------------------------+----------------+
position: [3], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 63 64 65 64 65 00 00 00 00 00 |cdede..... |
+--------+-------------------------------------------------+----------------+

===========================================================================================
再次写入
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 63 64 65 66 67 00 00 00 00 00 |cdefg..... |
+--------+-------------------------------------------------+----------------+

字符串与 ByteBuffer 的转换

这里主要介绍如何字符串转 ByteBuffer,且设置其编码

bytes to String

编码:字符串调用 getByte 方法获得 byte 数组,将 byte 数组放入 ByteBuffer 中 解码:先调用 ByteBuffer 的 flip 方法,然后通过 StandardCharsets 的 decoder 方法解码

public class Translate {
public static void main(String[] args) {
// 准备两个字符串
String str1 = "hello";
String str2 = "";

ByteBuffer buffer1 = ByteBuffer.allocate(16);
// 通过字符串的 getByte 方法获得字节数组,放入缓冲区中
buffer1.put(str1.getBytes());

// 将缓冲区中的数据转化为字符串
// 切换模式
buffer1.flip();

// 通过 StandardCharsets 解码,获得 CharBuffer,再通过 toString 获得字符串
str2 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str2);
}
}

直接通过编码转

编码:通过 StandardCharsets 的 encode 方法获得 ByteBuffer,此时获得的 ByteBuffer 为读模式,无需通过 flip 切换模式 解码:通过 StandardCharsets 的 decoder 方法解码

public class Translate {
public static void main(String[] args) {
// 准备两个字符串
String str1 = "hello";
String str2 = "";

// 通过 StandardCharsets 的 encode 方法获得 ByteBuffer
// 此时获得的 ByteBuffer 为读模式,无需通过 flip 切换模式
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode(str1);

// 将缓冲区中的数据转化为字符串
// 通过 StandardCharsets 解码,获得 CharBuffer,再通过 toString 获得字符串
str2 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str2);
}
}

通过 wrap 方法

编码:字符串调用 getByte() 方法获得字节数组,将字节数组传给 ByteBuffer 的 wrap() 方法,通过该方法获得ByteBuffer。同样无需调用 flip 方法切换为读模式

解码:通过 StandardCharsets 的 decoder 方法解码

public class Translate {
public static void main(String[] args) {
// 准备两个字符串
String str1 = "hello";
String str2 = "";

// 通过 StandardCharsets 的 encode 方法获得 ByteBuffer
// 此时获得的 ByteBuffer 为读模式,无需通过 flip 切换模式
ByteBuffer buffer1 = ByteBuffer.wrap(str1.getBytes());

// 将缓冲区中的数据转化为字符串
// 通过 StandardCharsets 解码,获得 CharBuffer,再通过 toString 获得字符串
str2 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str2);
}
}

粘包与半包

粘包与半包的现象

网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔 但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有 3 条为

  • Hello,world\n
  • I'm Nyima\n
  • How are you?\n

变成了下面的两个 byteBuffer(粘包,半包)

  • Hello,world\nI'm Nyima\nHo
  • w are you?\n

粘包出现原因

发送方在发送数据时,并不是一条一条地发送数据,而是 将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去

半包出现原因

接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将 信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象

解决办法

通过 get(index) 方法遍历 ByteBuffer,遇到分隔符时进行处理。

注意:get(index) 不会改变 position 的值

  • 记录该段数据长度,以便于申请对应大小的缓冲区
  • 将缓冲区的数据通过 get() 方法写入到 target 中

调用 compact 方法切换模式,因为缓冲区中可能还有未读的数据

public class ByteBufferDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(32);
// 模拟粘包+半包
buffer.put("Hello,world\nI'm Nyima\nHo".getBytes());
// 调用 split 函数处理
split(buffer);
buffer.put("w are you?\n".getBytes());
split(buffer);
}

private static void split(ByteBuffer buffer) {
// 切换为读模式(读模式 position 是从 0 开始的)
buffer.flip();
for(int i = 0; i < buffer.limit(); i++) {
// 遍历寻找分隔符
// get(i) 不会移动 position
if (buffer.get(i) == '\n') {
// 缓冲区长度
int length = i + 1 - buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
// 将前面的内容写入 target 缓冲区
for(int j = 0; j < length; j++) {
// 将 buffer 中的数据写入 target 中
target.put(buffer.get());
}
// 打印查看结果
ByteBufferUtil.debugAll(target);
}
}
// 切换为写模式,但是缓冲区可能未读完,这里需要使用 compact
buffer.compact();
}
}

原理就是,找到分隔符,然后把分隔符前面的内容存储到临时的 target 里面,这个 target 就可以是目标数据,然后把剩下的内容通过 compact 把剩下的内容前移,等待下次取得数据

输出内容为:

+--------+-------------------- all ------------------------+----------------+
position: [12], limit: [12]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 77 6f 72 6c 64 0a |Hello,world. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [10], limit: [10]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 49 27 6d 20 4e 79 69 6d 61 0a |I'm Nyima. |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 3f 0a |How are you?. |
+--------+-------------------------------------------------+----------------+